package com.swak.boot.loader;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collections;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

import org.springframework.core.DecoratingClassLoader;
import org.springframework.core.OverridingClassLoader;
import org.springframework.core.io.FileSystemResource;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.FileCopyUtils;

import com.swak.utils.FileUtils;
import com.swak.utils.Sets;
import com.swak.utils.StringUtils;

/**
 * 有问题：支持jar 的方式过于消耗性能，直接使用Class的模式，用于临时处理类覆盖问题 <br>
 * 
 * 优先加载指定：jars 下的类 <br>
 * 有一种解决方案是： 打包之后将 lib 放入更目录下个，更新jar时直接可以覆盖之前的jar包 <br>
 * 另外一种解决方案是： 将默认的类加载器设置为此类加载，优先会加载jars目录下的jar<br>
 * 
 * 可以通过此方法实现加密部分jar： 应该击沉 URLClassLoader
 * 
 * @author lifeng
 */
public class PriorityClassLoader extends DecoratingClassLoader {

	private static final String JARS_DIR = "priorcls";
	private final Set<String> excludedPackages = Collections.newSetFromMap(new ConcurrentHashMap<>(8));
	private final Set<String> excludedClasses = Collections.newSetFromMap(new ConcurrentHashMap<>(8));
	private static final String CLASS_FILE_SUFFIX = ".class";
	private static Set<String> CLSS = Sets.newHashSet();
	private static File JARS;

	static {
		ClassLoader.registerAsParallelCapable();
		try {
			FileSystemResource resource = new FileSystemResource(StringUtils.EMPTY);
			File base = resource.getFile().getAbsoluteFile();
			JARS = new File(base, JARS_DIR);
			if (!JARS.exists()) {
				JARS.mkdirs();
			}
			String[] clss = JARS.list((dir, name) -> {
				return StringUtils.endsWith(name, CLASS_FILE_SUFFIX);
			});
			for (String cls : clss) {
				CLSS.add(StringUtils.removeEnd(cls, CLASS_FILE_SUFFIX));
			}
		} catch (Exception e) {
			// igone
		}
	}

	/**
	 * 解析基础路径: 默认的：parent 是 系统类加载器
	 */
	public PriorityClassLoader() {
		for (String packageName : OverridingClassLoader.DEFAULT_EXCLUDED_PACKAGES) {
			excludePackage(packageName);
		}
	}

	/**
	 * Add a package name to exclude from decoration (e.g. overriding).
	 * <p>
	 * Any class whose fully-qualified name starts with the name registered here
	 * will be handled by the parent ClassLoader in the usual fashion.
	 * 
	 * @param packageName the package name to exclude
	 */
	public void excludePackage(String packageName) {
		Assert.notNull(packageName, "Package name must not be null");
		this.excludedPackages.add(packageName);
	}

	/**
	 * Add a class name to exclude from decoration (e.g. overriding).
	 * <p>
	 * Any class name registered here will be handled by the parent ClassLoader in
	 * the usual fashion.
	 * 
	 * @param className the class name to exclude
	 */
	public void excludeClass(String className) {
		Assert.notNull(className, "Class name must not be null");
		this.excludedClasses.add(className);
	}

	/**
	 * Determine whether the specified class is excluded from decoration by this
	 * class loader.
	 * <p>
	 * The default implementation checks against excluded packages and classes.
	 * 
	 * @param className the class name to check
	 * @return whether the specified class is eligible
	 * @see #excludePackage
	 * @see #excludeClass
	 */
	protected boolean isExcluded(String className) {
		if (this.excludedClasses.contains(className)) {
			return true;
		}
		for (String packageName : this.excludedPackages) {
			if (className.startsWith(packageName)) {
				return true;
			}
		}
		return false;
	}

	@Override
	protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
		if (isEligibleForOverriding(name) && hasPriorityCls(name)) {
			Class<?> result = findLoadedClass(name);
			if (result != null) {
				return result;
			}
			result = this.loadClassForOverriding(name);
			if (result != null) {
				if (resolve) {
					resolveClass(result);
				}
				return result;
			}
		}
		return super.loadClass(name, resolve);
	}

	/**
	 * Determine whether the specified class is eligible for overriding by this
	 * class loader.
	 * 
	 * @param className the class name to check
	 * @return whether the specified class is eligible
	 * @see #isExcluded
	 */
	protected boolean isEligibleForOverriding(String className) {
		return !isExcluded(className);
	}

	/**
	 * 是否有需要优先加载的类
	 * 
	 * @param name
	 * @return
	 */
	protected boolean hasPriorityCls(String name) {
		return JARS != null && CLSS != null && CLSS.contains(name);
	}

	/**
	 * Load the specified class for overriding purposes in this ClassLoader.
	 * <p>
	 * The default implementation delegates to {@link #findLoadedClass},
	 * {@link #loadBytesForClass} and {@link #defineClass}.
	 * 
	 * @param name the name of the class
	 * @return the Class object, or {@code null} if no class defined for that name
	 * @throws ClassNotFoundException if the class for the given name couldn't be
	 *                                loaded
	 */
	@Nullable
	protected Class<?> loadClassForOverriding(String name) throws ClassNotFoundException {
		Class<?> result = findLoadedClass(name);
		if (result == null) {
			byte[] bytes = loadBytesForClass(name);
			if (bytes != null) {
				result = defineClass(name, bytes, 0, bytes.length);
			}
		}
		return result;
	}

	/**
	 * Load the defining bytes for the given class, to be turned into a Class object
	 * through a {@link #defineClass} call.
	 * <p>
	 * The default implementation delegates to {@link #openStreamForClass} and
	 * {@link #transformIfNecessary}.
	 * 
	 * @param name the name of the class
	 * @return the byte content (with transformers already applied), or {@code null}
	 *         if no class defined for that name
	 * @throws ClassNotFoundException if the class for the given name couldn't be
	 *                                loaded
	 */
	@Nullable
	protected byte[] loadBytesForClass(String name) throws ClassNotFoundException {
		InputStream is = openStreamForClass(name);
		if (is == null) {
			return null;
		}
		try {
			// Load the raw bytes.
			byte[] bytes = FileCopyUtils.copyToByteArray(is);
			// Transform if necessary and use the potentially transformed bytes.
			return transformIfNecessary(name, bytes);
		} catch (IOException ex) {
			throw new ClassNotFoundException("Cannot load resource for class [" + name + "]", ex);
		}
	}

	/**
	 * Open an InputStream for the specified class.
	 * <p>
	 * The default implementation loads a standard class file through the parent
	 * ClassLoader's {@code getResourceAsStream} method.
	 * 
	 * @param name the name of the class
	 * @return the InputStream containing the byte code for the specified class
	 * @throws FileNotFoundException
	 */
	@Nullable
	protected InputStream openStreamForClass(String name) {
		String internalName = name + CLASS_FILE_SUFFIX;
		return FileUtils.in(new File(JARS, internalName));
	}

	/**
	 * Transformation hook to be implemented by subclasses.
	 * <p>
	 * The default implementation simply returns the given bytes as-is.
	 * 
	 * @param name  the fully-qualified name of the class being transformed
	 * @param bytes the raw bytes of the class
	 * @return the transformed bytes (never {@code null}; same as the input bytes if
	 *         the transformation produced no changes)
	 */
	protected byte[] transformIfNecessary(String name, byte[] bytes) {
		return bytes;
	}
}